package com.jyniubi.gmail.gateway.filter;

import com.jyniubi.gmail.common.utils.IpUtils;
import com.jyniubi.gmail.common.utils.JwtUtils;
import com.jyniubi.gmail.gateway.config.JwtProperties;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.http.HttpCookie;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.springframework.util.MultiValueMap;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.List;
import java.util.Map;

@Component
@Slf4j
@EnableConfigurationProperties(JwtProperties.class)
public class AuthGatewayFilterFactory extends AbstractGatewayFilterFactory<AuthGatewayFilterFactory.PathConfig> {

    private static final String BASE_PATH = "http://sso.gmall.com/toLogin.html";
    private static final String RETURN = "?returnUrl=";
    @Autowired
    private JwtProperties properties;

    /**
     * 一定要重写构造方法
     * 告诉父类，这里使用PathConfig对象接收配置内容
     */
    public AuthGatewayFilterFactory() {
        super(PathConfig.class);
    }

    @Override
    public GatewayFilter apply(PathConfig config) {
        // 实现GatewaFilter接口
        return new GatewayFilter() {
            @Override
            public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
                log.info("进入局部过滤器" + config);
                ServerHttpRequest request = exchange.getRequest();
                ServerHttpResponse response = exchange.getResponse();
                // 这个是防止? & 被错误解析，所以进行编码，但我发现不进行编码也不会有问题，
                // 主要是在 判断是否在拦截名单中时没有将请求路径放在请求头中
                // http%3A%2F%2Fsso.gmall.com%2FtoLogin.html%3FreturnUrl%3Dhttp%3A%2F%2Fcart.gmall.com%2FaddCart.html%3FskuId%3D26%26count%3D1
                // 已解决
                // todo：解决跳转路径?skuId=11&count=1   &count=1会丢失的问题
                String rawReturnUrl = request.getURI().toString();
                String returnUrl;
                try {
                    returnUrl = URLEncoder.encode(rawReturnUrl, StandardCharsets.UTF_8.name());
                } catch (UnsupportedEncodingException e) {
                    log.error("URL编码失败", e);
                    response.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR);
                    return response.setComplete();
                }
                // 1. 判断当前请求路径在不在拦截名单，不在直接放行
                List<String> authPaths = config.getAuthPaths(); // 拦截路径
                String curPath = request.getURI().getPath(); // 当前请求的路径
                if (authPaths != null && authPaths.stream().noneMatch(curPath::startsWith)){
                    // 将编码后的 returnUrl 添加到请求头中
                    ServerHttpRequest modifiedRequest = request.mutate()
                            .header("returnUrl", returnUrl)
                            .build();
                    return chain.filter(exchange.mutate().request(modifiedRequest).build());
                }
                // 2. 获取token信息：同步请求cookie中获取，异步请求头信息中获取
                String token = request.getHeaders().getFirst(properties.getCookieName());
                if (StringUtils.isBlank(token)){
                    MultiValueMap<String, HttpCookie> cookies = request.getCookies();
                    if (!CollectionUtils.isEmpty(cookies) && cookies.containsKey(properties.getCookieName())){
                        token = cookies.getFirst(properties.getCookieName()).getValue();
                    }
                }

                // 3. 判断token是否为空，如果为空则重定向到登录页面，请求结束
                if (StringUtils.isBlank(token)) {
                    response.setStatusCode(HttpStatus.SEE_OTHER);
                    response.getHeaders().set(HttpHeaders.LOCATION,BASE_PATH + RETURN +  returnUrl);
                    return response.setComplete();
                }
                try {
                    // 4. 解析token，如果解析异常，也重定向到登录页面，请求结束
                    Map<String, Object> parseToken = JwtUtils.getInfoFromToken(token, properties.getPublicKey());
                    // 5. 获取token载荷中登录用户的ip地址 和当前请求的ip地址比较，如果不一致则重定向到登录页面，请求结束
                    String ip = parseToken.get("ip").toString();  // token 中的ip
                    String curIp = IpUtils.getIpAddressAtGateway(request); // 用户请求的ip
                    if (!StringUtils.equals(ip,curIp)) {
                        response.setStatusCode(HttpStatus.SEE_OTHER);
                        response.getHeaders().set(HttpHeaders.LOCATION,BASE_PATH + request.getURI());
                        return response.setComplete();
                    }
                    // 6. 把登录信息传递给后续服务  改变请求对象用mutate方法
                    request.mutate().header("userId",parseToken.get("userId").toString())
                            .header("username",parseToken.get("username").toString())
                            .build();
                    exchange.mutate().request(request).build();
                    // 7. 放行
                    return chain.filter(exchange);
                } catch (Exception e) {
                    log.error("解析token异常",e);
                    response.setStatusCode(HttpStatus.SEE_OTHER);
                    response.getHeaders().set(HttpHeaders.LOCATION,BASE_PATH + request.getURI());
                    return response.setComplete();
                }
            }
        };
    }

    /**
     * 指定字段顺序
     * 可以通过不同的字段分别读取：/toLogin.html,/login
     * 在这里希望通过一个集合字段读取所有的路径
     *
     * @return
     */
    @Override
    public List<String> shortcutFieldOrder() {
        return Arrays.asList("authPaths");
    }

    /**
     * 指定读取字段的结果集类型
     * 默认通过map的方式，把配置读取到不同字段
     * 例如：/toLogin.html,/login
     * 由于只指定了一个字段，只能接收/toLogin.html
     *
     * @return
     */
    @Override
    public ShortcutType shortcutType() {
        return ShortcutType.GATHER_LIST;
    }
    @Data
    public static class PathConfig {
        private List<String> authPaths;
    }
}
